﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;

namespace InfoPathHelpers.Cryptography
{
    /// <summary>
    /// Provides basic encryption/decryption for single nodes or entire forms.
    /// </summary>
    public class SymmetricKeyCryptography
    {
        /// <summary>
        /// Creates a new instance of the SymmetricKeyCryptography class with the supplied SymmetricAlgorithm.
        /// </summary>
        /// <param name="algorithm">The algorithm to use for encryption/decryption.</param>
        public SymmetricKeyCryptography(SymmetricAlgorithm algorithm)
            : this(algorithm, new List<string>(), new List<string>()) { }

        /// <summary>
        /// Creates a new instance of the SymmetricKeyCryptography class with the supplied SymmetricAlgorithm and lists of nodes/groups to ignore.
        /// </summary>
        /// <param name="algorithm">The algorithm to use for encryption/decryption.</param>
        /// <param name="nodesToIgnore">The list of local names for nodes which should not be processed during encryption/decryption.</param>
        /// <param name="groupsToIgnore">The list of local names for groups which should not be processed during encryption/decryption.</param>
        public SymmetricKeyCryptography(SymmetricAlgorithm algorithm, List<string> nodesToIgnore, List<string> groupsToIgnore)
        {
            if(algorithm == null)
            {
                throw new ArgumentNullException("key");
            }

            SymmetricAlgorithm = algorithm;
            NodesToIgnore = nodesToIgnore;
            GroupsToIgnore = groupsToIgnore;
        }

        /// <summary>
        /// Gets or sets the list of local names for nodes which should not be processed during encryption/decryption.
        /// </summary>
        public List<string> NodesToIgnore { get; set; }

        /// <summary>
        /// Gets or sets the list of local names for groups which should not be processed during encryption/decryption.
        /// </summary>
        public List<string> GroupsToIgnore { get; set; }

        /// <summary>
        /// Gets or sets the key to be used for encryption/decryption.
        /// </summary>
        public SymmetricAlgorithm SymmetricAlgorithm { get; set; }

        /// <summary>
        /// Encrypts the value of the supplied node using the supplied symmetric algorithm.
        /// </summary>
        /// <param name="node">The node to encrypt.</param>
        /// <param name="Key">The algorithm to use for encryption.</param>
        public void EncryptNode(XPathNavigator node)
        {
            if (NodeHelpers.NodeIsGroup(node))
            {
                throw new ArgumentException("Must be a simple element to perform encryption", "node");
            }
            
            // Empty nodes require no encryption so just return.
            if (NodeHelpers.NodeIsNullOrEmpty(node))
                return;

            else InternalEncryptNode(node);
        }

        /// <summary>
        /// Decrypts the value of the supplied node using the supplied symmetric algorithm.
        /// </summary>
        /// <param name="node">The node to decrypt.</param>
        /// <param name="Key">The algorithm to use for decryption.</param>
        public void DecryptNode(XPathNavigator node)
        {
            if (NodeHelpers.NodeIsGroup(node))
            {
                throw new ArgumentException("Must be a simple element to perform decryption", "node");
            }

            // Empty nodes require no decryption so just return.
            if (NodeHelpers.NodeIsNullOrEmpty(node))
                return;

            InternalDecryptNode(node);
        }

        /// <summary>
        /// Loops through all children of the supplied node and encrypts the values.
        /// </summary>
        /// <param name="rootNode">The node whos children are to be encrypted.</param>
        public void EncryptChildNodes(XPathNavigator rootNode, params string[] nodesToIgnore)
        {
            NodesToIgnore = new List<string>(nodesToIgnore);
            EncryptChildNodes(rootNode);
        }

        /// <summary>
        /// Loops through all children of the supplied node and encrypts the values.
        /// </summary>
        /// <param name="rootNode">The node whos children are to be encrypted.</param>
        public void EncryptChildNodes(XPathNavigator rootNode, List<string> nodesToIgnore, List<string> groupsToIgnore)
        {
            NodeHelpers.ProcessAllNodes(rootNode, nodesToIgnore, groupsToIgnore, node => InternalEncryptNode(node));
        }

        /// <summary>
        /// Loops through all children of the supplied node and decrypts the values.
        /// </summary>
        /// <param name="rootNode">The node whos children are to be decrypted.</param>
        public void DecryptChildNodes(XPathNavigator rootNode, params string[] nodesToIgnore)
        {
            NodesToIgnore = new List<string>(nodesToIgnore);
            DecryptChildNodes(rootNode);
        }

        /// <summary>
        /// Loops through all children of the supplied node and decrypts the values.
        /// </summary>
        /// <param name="rootNode">The node whos children are to be decrypted.</param>
        public void DecryptChildNodes(XPathNavigator rootNode)
        {
            NodeHelpers.ProcessAllNodes(rootNode, NodesToIgnore, GroupsToIgnore, node => InternalDecryptNode(node));
        }

        private void InternalEncryptNode(XPathNavigator node)
        {
            var Buffer = new MemoryStream();
            byte[] NodeValueBytes = System.Text.Encoding.UTF8.GetBytes(node.Value);
            using (var CryptoStream = new CryptoStream(Buffer, SymmetricAlgorithm.CreateEncryptor(), CryptoStreamMode.Write))
            {
                CryptoStream.Write(NodeValueBytes, 0, NodeValueBytes.Length);
            }
            node.SetValue(Convert.ToBase64String(Buffer.ToArray()));
        }

        private void InternalDecryptNode(XPathNavigator node)
        {
            var Buffer = new MemoryStream();
            try
            {
                byte[] NodeValueBytes = Convert.FromBase64String(node.Value);
                using (var CryptoStream = new CryptoStream(Buffer, SymmetricAlgorithm.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    CryptoStream.Write(NodeValueBytes, 0, NodeValueBytes.Length);
                }
                node.SetValue(System.Text.Encoding.UTF8.GetString(Buffer.ToArray()));
            }
            catch (FormatException)
            {
                throw new CryptographicException(string.Format("The node \"{0}\" could not be decrypted, the value was not of the correct format.", node.LocalName));
            }
        }


        /// <summary>
        /// Encrypts the values of all elements and attibutes of the supplied <paramref name="form"/>.
        /// </summary>
        /// <param name="formContext">The FormContext representing the form to encrypt 
        public void EncryptDocument(FormContext formContext)
        {
            EncryptDocument(formContext, false);
        }

        /// <summary>
        /// Encrypts the values of all elements and attibutes of the supplied <paramref name="formContext"/>,
        /// with the option to automatically skip non string nodes.
        /// </summary>
        /// <param name="formContext">The FormContext representing the form to encrypt 
        /// <param name="isSchemaAware">A boolean value indicating whether or not encrption 
        /// should be aware of the form schema. 
        /// This must be set to true for forms with non string fields, or encryption will cause validation errors. 
        /// Alternatively use nodesToIgnore to ignore non string fields.</param>
        public void EncryptDocument(FormContext formContext, bool isSchemaAware)
        {
            XPathNavigator RootNode = formContext.Navigator;
            RootNode.MoveToChild(XPathNodeType.Element);

            if (isSchemaAware)
            {
                NodeHelpers.ProcessAllNodes(RootNode, NodesToIgnore, GroupsToIgnore,
                   node => SchemaAwareProcessNode(FormInfo.LoadSchemaSet(formContext), node,
                       process => InternalEncryptNode(node)));
            }
            else
            {
                EncryptChildNodes(RootNode);
            }
        }

        /// <summary>
        /// Decrypts the values of all elements and attibutes of the supplied <paramref name="formContext"/>.
        /// </summary>
        /// <param name="formContext">The FormContext representing the form to decrypt.
        public void DecryptDocument(FormContext formContext)
        {
            DecryptDocument(formContext, false);
        }

        /// <summary>
        /// Decrypts the values of all elements and attibutes of the supplied <paramref name="formContext"/>, 
        /// with the option to automatically skip non string nodes.
        /// </summary>
        /// <param name="formContext">The FormContext representing the form to decrypt.
        /// <param name="isSchemaAware">A boolean value indicating whether or not decrption 
        /// should be aware of the form schema. 
        /// This must be set to true for forms with non string fields, or decryption will fail. 
        /// Alternatively use NodesToIgnore to ignore non string fields.</param>
        public void DecryptDocument(FormContext formContext, bool isSchemaAware)
        {
            XPathNavigator RootNode = formContext.Navigator;
            RootNode.MoveToChild(XPathNodeType.Element);

            if (isSchemaAware)
            {
                NodeHelpers.ProcessAllNodes(RootNode, NodesToIgnore, GroupsToIgnore,
                    node => SchemaAwareProcessNode(FormInfo.LoadSchemaSet(formContext), node,
                        process => InternalDecryptNode(node)));
            }
            else
            {
                DecryptChildNodes(RootNode);
            }
        }

        private void SchemaAwareProcessNode(XmlSchemaSet schemaSet, XPathNavigator node, NodeHelpers.ProcessNodeDelegate processNode)
        {
            XmlQualifiedName QualifiedName = new XmlQualifiedName(node.LocalName, node.NamespaceURI);

            XmlSchemaType Type = null;

            if (node.NodeType == XPathNodeType.Attribute)
            {
                XmlSchemaAttribute Attribute = (XmlSchemaAttribute)schemaSet.GlobalAttributes[QualifiedName];
                Type = Attribute.AttributeSchemaType;
            }
            else
            {
                XmlSchemaElement Element = (XmlSchemaElement)schemaSet.GlobalElements[QualifiedName];
                Type = Element.ElementSchemaType;

            }

            if (Type.TypeCode != XmlTypeCode.String)
                return;

            processNode(node);
        }
    }
}
